/*
 * Copyright (C) 2009 M. Hautle.
 * 
 * This file is part of TrackedBean.
 * 
 * TrackedBean is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free
 * Software Foundation, either version 3 of the License, or (at your option) any later version.
 * 
 * TrackedBean is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
 * A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public License along with TrackedBean. If not, see <http://www.gnu.org/licenses/>.
 */
package ch.trackedbean.copier.internal;

import static ch.simpleel.util.MiscUtils.*;
import java.util.*;
import java.util.Map.Entry;
import ch.trackedbean.copier.*;
import ch.trackedbean.copier.annotations.*;

/**
 * Mapping engine.<br>
 * The mapper is thread safe after the configuration has been done.
 * 
 * @author M. Hautle
 */
public class BeanMapper implements IBeanMapper {
    /** The base class specified in {@link SourceClass} or null. */
    private final Class mappedBaseType;

    /** The destination class. */
    private final Class dstType;

    /** The mappings in this mapper. Key = Base class on which the mappings get applied. */
    private final Map<Class, List<IMappingHolder>> mappings = new HashMap<Class, List<IMappingHolder>>();

    /** Destination property to {@link IMappingHolder}s mapping. */
    private final Map<String, IMappingHolder> dstMappingInfos = new HashMap<String, IMappingHolder>();

    /**
     * Default constructor.
     * 
     * @param mappedBaseType The mapped type (from which to copy the data, may be null)
     * @param dstType The destination type (to which to copy the data)
     */
    public BeanMapper(Class mappedBaseType, Class dstType) {
        this.mappedBaseType = mappedBaseType;
        this.dstType = dstType;
    }

    /**
     * {@inheritDoc}
     */
    public boolean doesSomething() {
        return mappings.size() > 0;
    }

    /**
     * {@inheritDoc}
     */
    public Class getMappedBaseType() {
        return mappedBaseType;
    }

    /**
     * {@inheritDoc}
     */
    public Class getDstType() {
        return dstType;
    }

    /**
     * {@inheritDoc}
     */
    public <T> T copySrc2Dst(Object src, T dst, IMappingContext context) {
        // store relation for cycle detection
        context.putElement(src, dst);
        // push current source on stack
        context.stepIntoObject(src);
        // process the whole type hierarchy of the passed source object
        for (Class<?> type = src.getClass(); type != null; type = type.getSuperclass()) {
            final List<IMappingHolder> mappings = this.mappings.get(type);
            if (mappings == null)
                continue;
            for (IMappingHolder m : mappings) {
                if (context.copySrc2Dst(m))
                    m.copySrc2Dst(src, dst, context);
            }
        }
        // pop current source from stack
        context.stepOutOfObject();
        return dst;
    }

    /**
     * {@inheritDoc}
     */
    public <T> T copyDst2Src(Object dst, T src, IMappingContext context) {
        // store relation for cycle detection
        context.putElement(dst, src);
        // push current source on stack
        context.stepIntoObject(dst);
        // process the whole type hierarchy of the passed source object
        for (Class<?> type = src.getClass(); type != null; type = type.getSuperclass()) {
            final List<IMappingHolder> mappings = this.mappings.get(type);
            if (mappings == null)
                continue;
            for (IMappingHolder m : mappings) {
                if (context.copyDst2Src(m))
                    m.copyDst2Src(src, dst, context);
            }
        }
        // pop current source from stack
        context.stepOutOfObject();
        return src;
    }

    /**
     * {@inheritDoc}
     */
    public IMappingHolder getMappingHolder(String property) {
        return dstMappingInfos.get(property);
    }

    /**
     * {@inheritDoc}
     */
    public List<IMappingHolder> getMappingHolders(boolean includeInherited) {
        if (mappedBaseType == null)
            return Collections.emptyList();
        return getMappingHolders(mappedBaseType, includeInherited);
    }

    /**
     * {@inheritDoc}
     */
    public List<IMappingHolder> getMappingHolders(Class baseClass, boolean includeInherited) {
        if (!includeInherited)
            return getMappingHolders(baseClass);
        final List<IMappingHolder> props = new ArrayList<IMappingHolder>();
        // process the whole type hierarchy of the passed base class
        for (Class<?> type = baseClass; type != null; type = type.getSuperclass()) {
            final List<IMappingHolder> mappings = this.mappings.get(type);
            if (mappings != null)
                props.addAll(mappings);
        }
        return props;
    }

    /**
     * {@inheritDoc}
     */
    public List<IMappingHolder> getMappingHolders(Class baseClass) {
        final List<IMappingHolder> mappings = this.mappings.get(baseClass);
        if (mappings == null)
            return Collections.emptyList();
        return new ArrayList<IMappingHolder>(mappings);
    }

    /**
     * Adds the given mapping for the given base type.
     * 
     * @param type The type on which to register the mapping
     * @param property The property name concerned by the given mapping
     * @param mapping The mapping
     */
    public void addMapping(Class type, String property, IMappingHolder mapping) {
        getOrCreateList(type, mappings).add(mapping);
        dstMappingInfos.put(property, mapping);
    }

    /**
     * Appends the mapping information from the given {@link IBeanMapper}.
     * 
     * @param mapper The mapper
     */
    public void addMappings(BeanMapper mapper) {
        for (Entry<Class, List<IMappingHolder>> e : mapper.mappings.entrySet()) {
            final Class<?> key = e.getKey();
            final List<IMappingHolder> list = getOrCreateList(key, mappings);
            // add the mapper information
            for (IMappingHolder m : e.getValue()) {
                list.add(m);
                dstMappingInfos.put(m.getDestinationAccessor().getPath(), m);
            }
        }
    }
}
